abidibo.net

HowTo tree like ordering in django admin changelist view

django django-admin

Premetto che questo è il frutto di almeno 6 ore di delirio. Chiaro I'm a newbie in django developement, ma cazzo quanto ho sudato, inoltre questo problema mi ha indotto a spulciare molte porzioni di codice del core di django, estendere viste, manager, costruire template tags, ed alcune di queste cose neanche sapevo esistessero,.. ne consegue un post delirante, prolisso e a tratti ripetitivo in cui cerco di spiegare e spiegarmi alcuni aspetti del funzionamento della admin di django.
Tra l'altro seguo premettendo che essendo ancora un pippone di django scriverò qui alcune banalità e probabilmente molte imprecisioni, ma me ne fotto, Cerco di spiegare con parole semplici quello che ho capito, o anche solo intuito, e non approfondito per mancanza di tempo. Pertanto vi invito a correggermi, a sottolineare le imperfezioni o a scrivere le vostre osservazioni nei commenti , di modo che possa renderlo più completo e preciso.

Comunque il succo è questo:
sto cercando di costruirmi un'applicazione di tipo "allegati", in quanto dopo la breve esperienza con django-filer ed amici belli ho pensato che sarebbe stato meglio scrivermi un filemanager a misura, io non ho bisogno per il momento di anteprime o cose fantascientifiche, ma di un modo per creare, rinominare e spostare cartelle all'interno delle quali caricarmi dei file da richiamare nelle varie sezioni del sito.
Per gestire il back-office di questaapplicazione ho scelto di utilizzare l'admin di django e di customizzarlo all'occorenza per ottenere quello che mi serve. Dopo aver dovuto sovrsascrivere i metodi di salvataggio ed eliminazione del modello per la scrittura su filesystem (il modello dell'oggetto cartella non è altro che il modello che descrive categorie ad albero infiinito) mi sono imabattuto in un problema che ritenevo in principio di più facile soluzione.

Il problema

Ottenere una visualizzazione ad albero (con futura indentazione) nella vista changelist dell'amministrazione di django, esempio

folder1
  subfodlder11
folder2
folder3
  subfolder31
  subfolder32

Chiaramente si tratta di un problema di ordinamento. Il tutto si riduce a poter definire un ordinamento completamente custom degli elementi listati all'interno della pagina, il classico ORDER BY FIELD(id, 1, 3, 5, 9) di mysql.

Considerazioni preliminari

Chi è che stampa questa famosa lista (che poi è una tabella)?

Semplicemente il template admin/change_list.html, ma...
...l'html che genera la tabella attraverso l'iterazione degli oggetti di tipo Model non è esplicitato qui. Guardando con un minimo di attenzione si scopre che in realtà chi si occupa di stampare quella porzione di template è un custom templatetag che si chiama result_list e che prende come parametro un bizzarro "cl".

Com'è fatto questo template tag?

Il template tag (che si trova dentro contrib/admin/templatetags/admin_list.py) è fatto così:

 

def result_list(cl):
  """
  Displays the headers and data list together
  """
  return {'cl': cl,
    'result_hidden_fields': list(result_hidden_fields(cl)),
    'result_headers': list(result_headers(cl)),
    'results': list(results(cl))}
result_list = register.inclusion_tag("admin/change_list_results.html")(result_list)

 

E cosa capiamo da qui?
Capiamo innanzi tutto che il template che si occupa di stampare il risultato si chiama change_list_result.html, e se lo andiamo a guardare (si trova nella stessa directory del template dal quale siamo partiti), notiamo che gli oggetti che generano la lista che ci interessa stanno dentro la variabile results. Questa variabile è una lista di oggetti restituiti dalla funzione definita poco sopra alla definizione del template tag, che guarda a caso si chiama results. Inoltre ci accorgiamo che questi oggetti contengono tutti i campi che devono essere visualizzati (definiti dalla proprietà list_display del ModelAdmin) + il checkbox per selezionare gli elementi da sottoporre ad un'eventuale azione, e tutto ciò con tanto di tag td inclusi (ce ne accorgiamo perché nel template tutte queste cose non ci sono, vengono richiamati solo gli item di results e quindi essi stessi devono contenerle).

Se si va a guardare questa funzione results

 

def results(cl):
  if cl.formset:
    for res, form in zip(cl.result_list, cl.formset.forms):
      yield ResultList(form, items_for_result(cl, res, form))
  else:
    for res in cl.result_list:
      yield ResultList(None, items_for_result(cl, res, None))

 

notiamo che cicla sugli elementi di cl.result_list (vi ricordate il bizzarro cl?) e ritorna un generator i cui elementi sono i valori sputati dalla funzione items_for_result che prende (tra gli altri parametri) proprio questi elementi. Ora non sto a postare il codice di quest'altra funzione (non vorrei finire la carta), ma se andate a vedervela vedrete che fa proprio quel che ci aspettavamo: ritorna un oggetto che contiene i campi da visualizzare, il checkbox etc... con tanto di formattazione delle celle di tabella.

Bene, allora abbiamo scovato l'origine degli oggetti della lista?

Vero, tant'è che se provate a stampare nel template un campo esistente di un oggetto della lista cl.result_list otterete il valore salvato su db. Quindi i nostri oggetti stanno in cl.result_list

Soluzione 1

A questo punto mi è venuto in mente che avrei potuto lavorare qui per risolvere il problema, cioè avrei potuto riordinare la lista cl.result_list per ottenere ciò che volevo. Alla fine non ho seguito questa strada, ho deciso di cercare di risolvere il tutto più a monte, comunque credo che potrebbe essere una buona soluzione.

Si tratterebbe di sovrascrivere parte del template change_list.html in modo da sostituire il tag result_list con un nostro tag personalizzato che ci andiamo a creare. Per crearlo utilizziamo (importandole) tutte le funzioni usate dal tag originale eccetto la funzione results che riscriviamo per aggiungere il codice necessario a formulare un nuovo ordinamento.

Non mi convince, proseguiamo

Vado giù di google e documentazione a manetta, so già che si può definire un ordinamento semplice tramite la tipica classe che estende ModelAdmin e che viene passata insieme al modello nel momento della registrazione dell'area amministrativa (d'ora in poi la chiamerò classe tipo ModelAdmin), ma non è il mio caso, non me la cavo con un ordinamento su uno o più campi. Però leggendo più attentamente le reference sulla classe ModelAdmin mi rendo conto che esite il metodo queryset e che serve a quanto pare a definire quali oggetti devono comparire nella pagina di changelist. Allora penso boh, sono a posto.

Sovrascrivo il metodo queryset della mia classe tipo ModelAdmin ed inizio a giocare un pochino filtrando la queryset e selezionando solamente le cartelle di primo livello (quelle con parent nullo). Effettivamente noto che nella lista compaiono solo più quelle, ed allora comincio a gasarmi. Google a manetta e trovo come concatenare queryset (per ricavare l'ordinamento tree ho bisogno di una ricorsione), ancora google e trovo come inizializzare una queryset empty. Bene ho tutti gli strumenti, implemento il tutto e bestemmio.
Non funziona una mazza, cioè gli oggetti selezionati e solo quelli compaiono, ma l'ordinamento continua ad essere quello di prima.
Bestemmio, e mi chiedo che cazzo possa essere che non va.

Preso dallo sconforto continuo a vagare per la documentazione e mi viene in mente che avevo visto di sfuggita un metodo della classe ModelAdmin (sempre lei) che si chiama changelist_view e penso che, sovrascrivendolo identico ma modificando la parte che passa al template gli oggetti, potrei ordinarli come più mi aggrada.

Quindi vado a vedermi prima il codice di questa funzione che sta all'interno della definizione della classe nel file contrib/admin/options.py.
Cazzo figo!
Scopro chi è cl, sempre lui, quello che poi viene passato ai template, quello che ha tra le sue proprietà la lista di oggetti che voglio riordinare.

Chi è cl?

cl è l'istanza di una classe ottenuta attraverso il metodo get_changelist, il quale ritorna la classe ChangeList. Bene, allora vado a vedermi come è fatta questa classe (è una vista, si trova nel file contrib/admin/views/main.py) e come pensavo scopro che ha una proprietà che si chiama result_list!
Come un segugio passo da una linea all'altra per arrivare all'origine, quindi...
result_list viene definita come il risultato del metodo _clone() applicato alla proprietà query_set, la quale viene definita attraverso il metodo get_query_set il quale la costruisce a partire dalla proprietà root_query_set la quale viene definita nel costruttore uguale a attenzione attenzione:

self.root_query_set = model_admin.queryset(request)

Allora penso "ci siamo cazzo, ho chiuso il cerchio". Quindi gli oggetti utilizzati per stampare la lista (cl.result_list) sono una manipolazione della queryset definita tramite l'omonimo metodo nella classe che estende ModelAdmin.

Allora mi incazzo.Dico io, perché l'ordinamento non funzionava?
Mi guardo meglio tutti i passaggi che trasformano la queryset nella lista di oggetti, ed in particolare noto che all'interno del metodo get_query_set ci sono queste 2 linee

# Set ordering.
if self.order_field:
  qs = qs.order_by('%s%s' % ((self.order_type == 'desc' and '-' or ''), self.order_field))

e capisco che semplicemente ogni mio tentativo di dare un ordinamento alla queryset a monte veniva qui sovrascritto. Difatti provando a commentare queste linee ed impostando un ordinamento per nome della cartella questi funzionava. Poi penso a "if self.order_field" e vado a vedermi come è definita la proprietà order_field, e scopro che lei e la sua amica order_type sono settate attraverso la funzione get_ordering la quale tra i suoi commenti annovera

# For ordering, first check the "ordering" parameter in the admin
# options, then check the object's default ordering. If neither of
# those exist, order descending by ID by default. Finally, look for
# manually-specified ordering from the query string.

e qui capisco che per arrivare al mio scopo devo neutralizzare questa funzione.

Come faccio, la sparo?

Non voglio chiaramente modificare la classe originale per far tornare due stringhe vuote alla funzione get_ordering, quindi devo crearmi una nuova classe che estenda ChangeList e sovrascriva il metodo. Inoltre questa nuova classe deve diventare cl,  quindi devo sovrascrivere anche il metodo get_changelist della mia classe di tipo ModelAdmin di modo che ritorni la mia ChangeList estesa, ed ecco qua:
in myapp/views.py

 

from django.contrib.admin.views.main import ChangeList

class folderChangeList(ChangeList):

  def get_ordering(self):
    return '', ''

 

in myapp/admin.py

 

from abi_filemanager.models import afmFolder, afmFile

from django.contrib import admin
from django.conf import settings

from forms import afmFolderForm

class afmFolderAdmin(admin.ModelAdmin):
  ........
  def get_changelist(self, request, **kwargs):
    from views import folderChangeList
    return folderChangeList
........

 

a questo punto riprovo con la mia ricorsione per concatenare le queryset nell'ordine corretto e capisco di essere un coglione.
Chiaramente non funziona, e dopo aver esteso anche la classe Manager per ridefinire il metodo get_query_set, cercando di spostare la logica dell'ordinamento al suo interno ed aver capito che era l'ennesima puttanata perché se utilizzo delle queryset API all'interno di quel metodo vado in loop infinito siccome viene chiamato ad ogni <model>.objects.<function>, ho intuito quale poteva essere il problema.

Il problema è che stavo confondendo la queryset (un oggetto di tipo QuerySet) con una lista di risultati che arrivano dal database. Una queryset è un oggetto che contiene le informazioni per ottenere una query. Non l'ho indagata, ma me la immagino come un oggetto fatto di tanti blocchi, i quali sono le parti costituenti di una query. in questo modo una queryset può essere dinamicamente e continuamente cambiata, a passi successivi senza dover eseguire i salti mortali su una query (cioè una stringa). La queryset sa se ritornerà un oggetto unico o una lista, ma non contiene dati fino a quando non viene percorsa ad esempio, o fino a quando non vengono chiamate determinate funzioni. Quando la queryset passa in azione, tutti i vari blocchi si compongono in modo da formare la query finale. Se io concateno queryset in modo da dare un certo ordine, per esempio concateno tante queryset che danno un unico risultato, e poi mi aspetto di trovare i risultati della query proprio in quell'ordine mi sto sbagliando. Chi comanda il blocco ORDER (chiamiamolo così nel mio delirio) della query non è l'ordine di concatenazione della queryset, ma le sue opportune funzioni (order_by ad esempio).
Allora tento utilizzando il metodo extra delle queryset, definendo un ORDER BY FIELD e prego che questo in qualche modo influisca sul blocco che ho chiamato ORDER, e per fortuna cosi è. Ecco il metodo queryset della classe che estende ModelAdmin

def queryset(self, request):
  orderid_list = []
  for obj in afmFolder.objects.filter(parent=None).order_by('name'):
    orderid_list.append(obj.pk)
    for child in obj.get_children_list():
      orderid_list.append(child.pk)

  ordering = 'FIELD(id,%s)' % ','.join(str(id) for id in orderid_list)
  qs = super(afmFolderAdmin, self).queryset(request).extra(select={'ordering': ordering}, order_by=('ordering',))

  return qs

Chiaramente get_children_list è un metodo del modello che restituisce la lista di tutti i child di un oggetto rispettando l'ordinamento visivo di tipo tree.

 

def get_children_list(self, child_list=None):
  if child_list is None:
    child_list = []
  children = afmFolder.objects.filter(parent=self)
  if children:
    for child in children:
      child_list.append(child)
      child.get_children_list(child_list)

  return child_list

 

Conclusione

Per ottenere un ordinamento di tipo tree nella vista changelist dell'area amministrativa di django per un modello si può fare nel seguente modo:

  • estendere la classe ChangeView di modo da sopprimere l'ordinamento a valle della queryset controllabile solamente tramite la proprietà ordering della classe che estende ModelAdmin e quindi insufficiente nel nostro caso
  • sovrascrivere il metodo get_changelist della classe ModelAdmin in modo da ritornare proprio la classe precedentemente creata
  • definire l'ordinamento della queryset all'interno del metodo queryset della classe che estende ModelAdmin, perché tale queryset è quello che da origine agli oggetti che creano la lista nel template ed il suo ordinamento non viene ulteriormente modificao grazie ai 2 punti precedenti.

Faccio notare che come ho letto da molte parti un'altra soluzione consisterebbe nell'aggiungere un extrafield al nostro modello per registrare un ordinamento custom. Ma in questo caso, a parte il fatto che non mi piace pensare di dover ricorrere a dati extra se posso evitarlo, la gestione di tale campo (se intero seppur zero-filled) sarebbe stata una palla allucinante siccome i sottorami possono essere spostati, eliminati, modificati etc..., e ad ogni operazione di questo tipo dovrebbe corrispondere un riassestamento dei valori del campo di ordinamento. Forse nel mio caso si poteva utilizzare un campo extra di tipo varchar dove conservare la concatenazione dei nomi di tutti i parent dal primo avo all'oggeto in questione e poi ordinare tutto alfabeticamente.

Ricordo anche di tenere in conto la prima soluzione di cui sopra, cioè quella che prevede il riordino deigli oggetti a livello di templatetag.

Vi prego correggete tutte le cazzate che ho scritto qua e la.

Subscribe to abidibo.net!

If you want to stay up to date with new contents published on this blog, then just enter your email address, and you will receive blog updates! You can set you preferences and decide to receive emails only when articles are posted regarding a precise topic.

I promise, you'll never receive spam or advertising of any kind from this subscription, just content updates.

Subscribe to this blog

Comments are welcome!

blog comments powered by Disqus

Your Smartwatch Loves Tasker!

Your Smartwatch Loves Tasker!

Now available for purchase!

Featured